using System;
using ch.ethz.ssh2;
using ch.ethz.ssh2.crypto;
using ch.ethz.ssh2.crypto.cipher;
using ch.ethz.ssh2.crypto.dh;
using ch.ethz.ssh2.crypto.digest;
using ch.ethz.ssh2.log;
using ch.ethz.ssh2.packets;
using ch.ethz.ssh2.signature;
using Org.BouncyCastle.Security;
using System.Threading;
namespace ch.ethz.ssh2.transport
{
	
	/// <summary> KexManager.
	/// 
	/// </summary>
	/// <author>  Christian Plattner
	/// </author>
	/// <version>  2.50, 03/15/10
	/// </version>
	public class KexManager
	{
		public static System.String[] DefaultServerHostkeyAlgorithmList
		{
			get
			{
				return new System.String[]{"ssh-rsa", "ssh-dss"};
			}
			
		}
		public static System.String[] DefaultKexAlgorithmList
		{
			get
			{
				return new System.String[]{"diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1"};
			}
			
		}
		//UPGRADE_NOTE: Final was removed from the declaration of 'log '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		//UPGRADE_NOTE: The initialization of  'log' was moved to static method 'ch.ethz.ssh2.transport.KexManager'. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1005'"
		private static readonly Logger log;
		
		internal KexState kxs;
		internal int kexCount = 0;
		internal KeyMaterial km;
		internal byte[] sessionId;
		internal ClientServerHello csh;
		
		//UPGRADE_NOTE: Final was removed from the declaration of 'accessLock '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		internal System.Object accessLock = new System.Object();
		internal ConnectionInfo lastConnInfo = null;
		
		internal bool connectionClosed = false;
		
		internal bool ignore_next_kex_packet = false;
		
		//UPGRADE_NOTE: Final was removed from the declaration of 'tm '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		internal TransportManager tm;
		
		internal CryptoWishList nextKEXcryptoWishList;
		internal DHGexParameters nextKEXdhgexParameters;
		
		internal ServerHostKeyVerifier verifier;
		//UPGRADE_NOTE: Final was removed from the declaration of 'hostname '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		internal System.String hostname;
		//UPGRADE_NOTE: Final was removed from the declaration of 'port '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		internal int port;
		//UPGRADE_NOTE: Final was removed from the declaration of 'rnd '. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1003'"
		internal SecureRandom rnd;
		
		public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, System.String hostname, int port, ServerHostKeyVerifier keyVerifier, SecureRandom rnd)
		{
			this.tm = tm;
			this.csh = csh;
			this.nextKEXcryptoWishList = initialCwl;
			this.nextKEXdhgexParameters = new DHGexParameters();
			this.hostname = hostname;
			this.port = port;
			this.verifier = keyVerifier;
			this.rnd = rnd;
		}
		
		public virtual ConnectionInfo getOrWaitForConnectionInfo(int minKexCount)
		{
			lock (accessLock)
			{
				while (true)
				{
					if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount))
						return lastConnInfo;
					
					if (connectionClosed)
						throw (System.IO.IOException) new System.IO.IOException("Key exchange was not finished, connection is closed.",tm.ReasonClosedCause);
					
					try
					{
						System.Threading.Monitor.Wait(accessLock);
					}
					catch (System.Threading.ThreadInterruptedException e)
					{
						Thread.CurrentThread.Interrupt();
					}
				}
			}
		}
		
		private System.String getFirstMatch(System.String[] client, System.String[] server)
		{
			if (client == null || server == null)
				throw new System.ArgumentException();
			
			if (client.Length == 0)
				return null;
			
			for (int i = 0; i < client.Length; i++)
			{
				for (int j = 0; j < server.Length; j++)
				{
					if (client[i].Equals(server[j]))
						return client[i];
				}
			}
			throw new NegotiateException();
		}
		
		private bool compareFirstOfNameList(System.String[] a, System.String[] b)
		{
			if (a == null || b == null)
				throw new System.ArgumentException();
			
			if ((a.Length == 0) && (b.Length == 0))
				return true;
			
			if ((a.Length == 0) || (b.Length == 0))
				return false;
			
			return (a[0].Equals(b[0]));
		}
		
		private bool isGuessOK(KexParameters cpar, KexParameters spar)
		{
			if (cpar == null || spar == null)
				throw new System.ArgumentException();
			
			if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false)
			{
				return false;
			}
			
			if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false)
			{
				return false;
			}
			
			/*
			* We do NOT check here if the other algorithms can be agreed on, this
			* is just a check if kex_algorithms and server_host_key_algorithms were
			* guessed right!
			*/
			
			return true;
		}
		
		private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server)
		{
			NegotiatedParameters np = new NegotiatedParameters();
			
			try
			{
				np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms);
				
				log.log(20, "kex_algo=" + np.kex_algo);
				
				np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms, server.server_host_key_algorithms);
				
				log.log(20, "server_host_key_algo=" + np.server_host_key_algo);
				
				np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server, server.encryption_algorithms_client_to_server);
				np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client, server.encryption_algorithms_server_to_client);
				
				log.log(20, "enc_algo_client_to_server=" + np.enc_algo_client_to_server);
				log.log(20, "enc_algo_server_to_client=" + np.enc_algo_server_to_client);
				
				np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server, server.mac_algorithms_client_to_server);
				np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client, server.mac_algorithms_server_to_client);
				
				log.log(20, "mac_algo_client_to_server=" + np.mac_algo_client_to_server);
				log.log(20, "mac_algo_server_to_client=" + np.mac_algo_server_to_client);
				
				np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server, server.compression_algorithms_client_to_server);
				np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client, server.compression_algorithms_server_to_client);
				
				log.log(20, "comp_algo_client_to_server=" + np.comp_algo_client_to_server);
				log.log(20, "comp_algo_server_to_client=" + np.comp_algo_server_to_client);
			}
			catch (NegotiateException e)
			{
				return null;
			}
			
			try
			{
				np.lang_client_to_server = getFirstMatch(client.languages_client_to_server, server.languages_client_to_server);
			}
			catch (NegotiateException e1)
			{
				np.lang_client_to_server = null;
			}
			
			try
			{
				np.lang_server_to_client = getFirstMatch(client.languages_server_to_client, server.languages_server_to_client);
			}
			catch (NegotiateException e2)
			{
				np.lang_server_to_client = null;
			}
			
			if (isGuessOK(client, server))
				np.guessOK = true;
			
			return np;
		}
		
		//UPGRADE_NOTE: Synchronized keyword was removed from method 'initiateKEX'. Lock expression was added. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1027'"
		public virtual void  initiateKEX(CryptoWishList cwl, DHGexParameters dhgex)
		{
			lock (this)
			{
				nextKEXcryptoWishList = cwl;
				nextKEXdhgexParameters = dhgex;
				
				if (kxs == null)
				{
					kxs = new KexState();
					
					kxs.dhgexParameters = nextKEXdhgexParameters;
					PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList, rnd);
					kxs.localKEX = kp;
					tm.sendKexMessage(kp.Payload);
				}
			}
		}
		
		private bool establishKeyMaterial()
		{
			try
			{
				int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server);
				int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server);
				int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server);
				
				int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client);
				int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client);
				int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client);
				
				km = KeyMaterial.create("SHA1", kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len, enc_sc_key_len, enc_sc_block_len, mac_sc_key_len);
			}
			catch (System.ArgumentException e)
			{
				return false;
			}
			return true;
		}
		
		private void  finishKex()
		{
			if (sessionId == null)
				sessionId = kxs.H;
			
			establishKeyMaterial();
			
			/* Tell the other side that we start using the new material */
			
			PacketNewKeys ign = new PacketNewKeys();
			tm.sendKexMessage(ign.Payload);
			
			BlockCipher cbc;
			MAC mac;
			
			try
			{
				cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server, km.initial_iv_client_to_server);
				
				mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server);
			}
			catch (System.ArgumentException e1)
			{
				throw new System.IO.IOException("Fatal error during MAC startup!");
			}
			
			tm.changeSendCipher(cbc, mac);
			tm.kexFinished();
		}
		
		public static void  checkServerHostkeyAlgorithmsList(System.String[] algos)
		{
			for (int i = 0; i < algos.Length; i++)
			{
				if (("ssh-rsa".Equals(algos[i]) == false) && ("ssh-dss".Equals(algos[i]) == false))
					throw new System.ArgumentException("Unknown server host key algorithm '" + algos[i] + "'");
			}
		}
		
		public static void  checkKexAlgorithmList(System.String[] algos)
		{
			for (int i = 0; i < algos.Length; i++)
			{
				if ("diffie-hellman-group-exchange-sha1".Equals(algos[i]))
					continue;
				
				if ("diffie-hellman-group14-sha1".Equals(algos[i]))
					continue;
				
				if ("diffie-hellman-group1-sha1".Equals(algos[i]))
					continue;
				
				throw new System.ArgumentException("Unknown kex algorithm '" + algos[i] + "'");
			}
		}
		
		private bool verifySignature(byte[] sig, byte[] hostkey)
		{
			if (kxs.np.server_host_key_algo.Equals("ssh-rsa"))
			{
				RSASignature rs = RSASHA1Verify.decodeSSHRSASignature(sig);
				RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey);
				
				log.log(50, "Verifying ssh-rsa signature");
				
				return RSASHA1Verify.verifySignature(kxs.H, rs, rpk);
			}
			
			if (kxs.np.server_host_key_algo.Equals("ssh-dss"))
			{
				DSASignature ds = DSASHA1Verify.decodeSSHDSASignature(sig);
				DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey);
				
				log.log(50, "Verifying ssh-dss signature");
				
				return DSASHA1Verify.verifySignature(kxs.H, ds, dpk);
			}
			
			throw new System.IO.IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'");
		}
		
		//UPGRADE_NOTE: Synchronized keyword was removed from method 'handleMessage'. Lock expression was added. "ms-help://MS.VSCC.v80/dv_commoner/local/redirect.htm?index='!DefaultContextWindowIndex'&keyword='jlca1027'"
		public virtual void  handleMessage(byte[] msg, int msglen)
		{
			lock (this)
			{
				PacketKexInit kip;
				
				if (msg == null)
				{
					lock (accessLock)
					{
						connectionClosed = true;
						System.Threading.Monitor.PulseAll(accessLock);
						return ;
					}
				}
				
				if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT))
					throw new System.IO.IOException("Unexpected KEX message (type " + msg[0] + ")");
				
				if (ignore_next_kex_packet)
				{
					ignore_next_kex_packet = false;
					return ;
				}
				
				if (msg[0] == Packets.SSH_MSG_KEXINIT)
				{
					if ((kxs != null) && (kxs.state != 0))
						throw new System.IO.IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!");
					
					if (kxs == null)
					{
						/*
						* Ah, OK, peer wants to do KEX. Let's be nice and play
						* together.
						*/
						kxs = new KexState();
						kxs.dhgexParameters = nextKEXdhgexParameters;
						kip = new PacketKexInit(nextKEXcryptoWishList, rnd);
						kxs.localKEX = kip;
						tm.sendKexMessage(kip.Payload);
					}
					
					kip = new PacketKexInit(msg, 0, msglen);
					kxs.remoteKEX = kip;
					
					kxs.np = mergeKexParameters(kxs.localKEX.KexParameters, kxs.remoteKEX.KexParameters);
					
					if (kxs.np == null)
						throw new System.IO.IOException("Cannot negotiate, proposals do not match.");
					
					if (kxs.remoteKEX.First_kex_packet_follows && (kxs.np.guessOK == false))
					{
						/*
						* Guess was wrong, we need to ignore the next kex packet.
						*/
						
						ignore_next_kex_packet = true;
					}
					
					if (kxs.np.kex_algo.Equals("diffie-hellman-group-exchange-sha1"))
					{
						if (kxs.dhgexParameters.Min_group_len == 0)
						{
							PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters);
							tm.sendKexMessage(dhgexreq.Payload);
						}
						else
						{
							PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters);
							tm.sendKexMessage(dhgexreq.Payload);
						}
						kxs.state = 1;
						return ;
					}
					
					if (kxs.np.kex_algo.Equals("diffie-hellman-group1-sha1") || kxs.np.kex_algo.Equals("diffie-hellman-group14-sha1"))
					{
						kxs.dhx = new DhExchange();
						
						if (kxs.np.kex_algo.Equals("diffie-hellman-group1-sha1"))
							kxs.dhx.init(1, rnd);
						else
							kxs.dhx.init(14, rnd);
						
						PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.E);
						tm.sendKexMessage(kp.Payload);
						kxs.state = 1;
						return ;
					}
					
					throw new System.SystemException("Unkown KEX method!");
				}
				
				if (msg[0] == Packets.SSH_MSG_NEWKEYS)
				{
					if (km == null)
						throw new System.IO.IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!");
					
					BlockCipher cbc;
					MAC mac;
					
					try
					{
						cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false, km.enc_key_server_to_client, km.initial_iv_server_to_client);
						
						mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client);
					}
					catch (System.ArgumentException e1)
					{
						throw new System.IO.IOException("Fatal error during MAC startup!");
					}
					
					tm.changeRecvCipher(cbc, mac);
					
					ConnectionInfo sci = new ConnectionInfo();
					
					kexCount++;
					
					sci.keyExchangeAlgorithm = kxs.np.kex_algo;
					sci.keyExchangeCounter = kexCount;
					sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server;
					sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client;
					sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server;
					sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client;
					sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo;
					sci.serverHostKey = kxs.hostkey;
					
					lock (accessLock)
					{
						lastConnInfo = sci;
						System.Threading.Monitor.PulseAll(accessLock);
					}
					
					kxs = null;
					return ;
				}
				
				if ((kxs == null) || (kxs.state == 0))
					throw new System.IO.IOException("Unexpected Kex submessage!");
				
				if (kxs.np.kex_algo.Equals("diffie-hellman-group-exchange-sha1"))
				{
					if (kxs.state == 1)
					{
						PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen);
						kxs.dhgx = new DhGroupExchange(dhgexgrp.P, dhgexgrp.G);
						kxs.dhgx.init(rnd);
						PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.E);
						tm.sendKexMessage(dhgexinit.Payload);
						kxs.state = 2;
						return ;
					}
					
					if (kxs.state == 2)
					{
						PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen);
						
						kxs.hostkey = dhgexrpl.HostKey;
						
						if (verifier != null)
						{
							bool vres = false;
							
							try
							{
								vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
							}
							catch (System.Exception e)
							{
								throw (System.IO.IOException) new System.IO.IOException("The server hostkey was not accepted by the verifier callback.",e);
							}
							
							if (vres == false)
								throw new System.IO.IOException("The server hostkey was not accepted by the verifier callback");
						}
						
						kxs.dhgx.F = dhgexrpl.F;
						
						try
						{
							kxs.H = kxs.dhgx.calculateH(csh.ClientString, csh.ServerString, kxs.localKEX.Payload, kxs.remoteKEX.Payload, dhgexrpl.HostKey, kxs.dhgexParameters);
						}
						catch (System.ArgumentException e)
						{
							throw (System.IO.IOException) new System.IO.IOException("KEX error.",e);
						}
						
						bool res = verifySignature(dhgexrpl.Signature, kxs.hostkey);
						
						if (res == false)
							throw new System.IO.IOException("Hostkey signature sent by remote is wrong!");
						
						kxs.K = kxs.dhgx.K;
						
						finishKex();
						kxs.state = - 1;
						return ;
					}
					
					throw new System.SystemException("Illegal State in KEX Exchange!");
				}
				
				if (kxs.np.kex_algo.Equals("diffie-hellman-group1-sha1") || kxs.np.kex_algo.Equals("diffie-hellman-group14-sha1"))
				{
					if (kxs.state == 1)
					{
						
						PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen);
						
						kxs.hostkey = dhr.HostKey;
						
						if (verifier != null)
						{
							bool vres = false;
							
							try
							{
								vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
							}
							catch (System.Exception e)
							{
								throw (System.IO.IOException) new System.IO.IOException("The server hostkey was not accepted by the verifier callback.",e);
							}
							
							if (vres == false)
								throw new System.IO.IOException("The server hostkey was not accepted by the verifier callback");
						}
						
						kxs.dhx.F = dhr.F;
						
						try
						{
							kxs.H = kxs.dhx.calculateH(csh.ClientString, csh.ServerString, kxs.localKEX.Payload, kxs.remoteKEX.Payload, dhr.HostKey);
						}
						catch (System.ArgumentException e)
						{
							throw (System.IO.IOException) new System.IO.IOException("KEX error.",e);
						}
						
						bool res = verifySignature(dhr.Signature, kxs.hostkey);
						
						if (res == false)
							throw new System.IO.IOException("Hostkey signature sent by remote is wrong!");
						
						kxs.K = kxs.dhx.K;
						
						finishKex();
						kxs.state = - 1;
						return ;
					}
				}
				
				throw new System.SystemException("Unkown KEX method! (" + kxs.np.kex_algo + ")");
			}
		}
		static KexManager()
		{
			log = Logger.getLogger(typeof(KexManager));
		}
	}
}